package ar.uba.fi.tonyvaliente.files;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * Representa un archivo de bloques de registros de longitud variable
 * @author Diego Morello
 */
public class File {
	
	private RandomAccessFile file;
	private FileHeader header;
	private boolean readOnly;
	
	private FileBlock lastBlock;
	
	/**
	 * Construye una instancia de File que se corresponde con un
	 * archivo de bloques previamente inexistente. La misma queda
	 * en modo lectura/escritura
	 * @param name nombre del archivo (path)
	 * @param blockSize tamanio de bloque para el archivo
	 * @param maxLoadFactorInsert maximo factor de carga de bloque para inserciones 
	 * @throws FileException si ocurre algun error al crear el archivo
	 * @throws IllegalArgumentException si maxLoadFactorInsert esta fuera del rango
	 * (0.0;0.1) o blockSize es menor o igual a cero
	 */
	public static File create(String name, int blockSize, float maxLoadFactorInsert)
			throws FileException {
		
		if (maxLoadFactorInsert <= 0.0f || maxLoadFactorInsert >= 1.0f || blockSize <= 0) {
			throw new FileException("Error al crear un File: el factor de carga maximo "
					+ "es incorrecto: " + maxLoadFactorInsert);
		}
		
		java.io.File osFile = new java.io.File(name);
		if (osFile.exists()) {
			throw new FileException("Error al crear un File: el archivo " + name + " ya existe");
		}
		
		return new File(name, blockSize, maxLoadFactorInsert);
	}
	
	/**
	 * Construye una instancia de File que se corresponde con un
	 * archivo de bloques previamente inexistente. La misma queda
	 * en modo lectura/escritura. El maximo factor de carga para el archivo
	 * sera DEFAULT_MAX_BLOCK_LOAD_FACTOR_FOR_INSERT
	 * @param name nombre del archivo (path)
	 * @param blockSize tamanio de bloque para el archivo
	 * @throws FileException si ocurre algun error al crear el archivo
	 * @throws IllegalArgumentException si blockSize es menor o igual a cero
	 */
	public static File create(String name, int blockSize) throws FileException {
		return create(name, blockSize, DEFAULT_MAX_BLOCK_LOAD_FACTOR_FOR_INSERT);
	}
	
	/**
	 * Construye una instancia de File que se corresponde con un
	 * archivo de bloques previamente inexistente. La misma queda
	 * en modo lectura/escritura. El maximo factor de carga para el archivo
	 * sera DEFAULT_MAX_BLOCK_LOAD_FACTOR_FOR_INSERT, y el tamaño de bloque
	 * sera DEFAULT_BLOCK_SIZE.
	 * @param name nombre del archivo (path)
	 * @param blockSize tamanio de bloque para el archivo
	 * @throws FileException si ocurre algun error al crear el archivo
	 * @throws IllegalArgumentException si blockSize es menor o igual a cero
	 */
	public static File create(String name) throws FileException {
		return create(name, DEFAULT_BLOCK_SIZE, DEFAULT_MAX_BLOCK_LOAD_FACTOR_FOR_INSERT);
	}
	
	/**
	 * Devuelve una instancia de File que se corresponde con un
	 * archivo de bloques ya existente.
	 * @param name nombre del archivo (path)
	 * @param readOnly indicador de solo lectura
	 * @throws FileException si ocurre algun error al abrir el archivo
	 */
	public static File open(String name, boolean readOnly) throws FileException {
		return new File(name, readOnly);
	}
	
	public int getBlockCount() {
		return header.getBlockCount();
	}
	
	public int getBlockSize() {
		return header.getBlockSize();
	}
	
	public boolean setProperty(String name, String value) throws FileException {
		if (readOnly) {
			throw new FileException("Imposible agregar/modificar propiedades: "
					+ "el archivo esta abierto en modo solo lectura");
		}
		
		if (header.setProperty(name, value)) {
			writeHeader();
			return true;
		} else {
			return false;
		}
	}
	
	public String getProperty(String name) {
		return header.getProperty(name);
	}
	
	public FileBlock getBlock(int pos) throws FileException {
		if (pos >= 0 && pos < getBlockCount()) {
			try {
				file.seek(calculateDiskOffset(pos));
				byte[] bytes = new byte[getBlockSize()];
				if (file.read(bytes) < getBlockSize()) {
					throw new FileException("Error al leer bloque: fin inesperado");
				}
				return FileBlock.createFromRawBytes(bytes);
			} catch (IOException e) {
				throw new FileException("Error de lectura", e);
			}
		} else if (pos == getBlockCount() && lastBlock != null) {
			return lastBlock;
		} else {
			throw new FileException("Posicion del File fuera de rango: " + pos);
		}
	}
	
	public void updateBlock(int pos, FileBlock block) throws FileException {
		if (readOnly) {
			throw new FileException("Imposible actualizar un bloque: el archivo esta "
					+ "abierto en modo de solo lectura");
		}
		
		if (pos >= 0 && pos < getBlockCount()) {
			try {
				file.seek(calculateDiskOffset(pos));
				file.write(block.toRawBytes());
			} catch (IOException e) {
				throw new FileException("Error al actualizar un bloque", e);
			}
		} else if (pos == getBlockCount() && lastBlock != null) {
			lastBlock = block;
		} else {
			throw new FileException("Posicion del File fuera de rango: " + pos);
		}
	}
	
	public RecordPosition addRecord(FileRecord r) throws FileException {
		if (readOnly) {
			throw new FileException("Imposible agregar un registro: el archivo esta "
					+ "abierto en modo de solo lectura");
		}
		
		if (lastBlock == null) {
			lastBlock = new FileBlock(this.header.getBlockSize());
		}
		
		int blockByteCount = lastBlock.getCapacity() - lastBlock.getFreeBytes();
		int maxBlockByteCount = (int)(lastBlock.getCapacity() * header.getMaxBlockLoadFactorInsert());
		if (blockByteCount + r.getData().length + 4 <= maxBlockByteCount) {
			lastBlock.appendRecord(r);
			return new RecordPosition(this.getBlockCount(), lastBlock.getRecordCount()-1);
		} else {
			flush();
			lastBlock = new FileBlock(this.header.getBlockSize());
			if (r.getData().length + 4 <= maxBlockByteCount) {
				lastBlock.appendRecord(r);
				return new RecordPosition(this.getBlockCount(), lastBlock.getRecordCount()-1);
			} else {
				throw new FileException("Imposible agregar un registro cuyo tamanio "
						+ "supera el del bloque");
			}
		}
	}
	
	public void flush() throws FileException {
		if (lastBlock != null) {
			appendBlock(lastBlock);
			lastBlock = null;
		}
	}
	
	public void close() throws FileException {
		try {
			flush();
			file.close();
			header = null;
		} catch (IOException e) {
			throw new FileException("Error al cerrar el archivo");
		}
	}
	
	private int appendBlock(FileBlock block) throws FileException {
		try {
			file.seek(file.length());
			file.write(block.toRawBytes());
			header.setBlockCount(header.getBlockCount() + 1);
			writeHeader();
			return header.getBlockCount() - 1;
		} catch (IOException e) {
			throw new FileException("Error al agregar un bloque al final del archivo", e);
		}
	}
	
	/**
	 * Construye una instancia de File que se corresponde con un
	 * archivo de bloques ya existente.
	 * @param name nombre del archivo (path)
	 * @param readOnly indicador de solo lectura
	 * @throws FileException si ocurre algun error al abrir el archivo
	 */
	private File(String name, boolean readOnly) throws FileException {
		// Verifico si el archivo existe
		java.io.File archivo = new java.io.File(name);
		if (archivo.exists() && archivo.isFile()) {
			String mode = (readOnly)? "r" : "rws";
			try {
				this.file = new RandomAccessFile(name, mode);
				this.readOnly = readOnly;
				this.readHeader();
				this.lastBlock = null;
			} catch (IOException e) {
				throw new FileException("Error al abrir un File", e);
			}
		} else {
			throw new FileException("Error al abrir un File: el archivo no existe - "
					+ name);
		}
	}
	
	/**
	 * Construye una instancia de File que se corresponde con un
	 * archivo de bloques previamente inexistente. La misma queda
	 * en modo lectura/escritura
	 * @param name nombre del archivo (path)
	 * @param blockSize tamanio de bloque para el archivo 
	 * @throws FileException si ocurre algun error al crear el archivo
	 */
	private File(String name, int blockSize, float maxLoadFactorInsert) throws FileException {
		try {
			this.file = new RandomAccessFile(name, "rws");
			this.readOnly = false;
			this.header = new FileHeader(blockSize, maxLoadFactorInsert);
			this.lastBlock = null;
			writeHeader();
		} catch (IOException e) {
			throw new FileException("Error al crear un File", e);
		}
	}
	
	private void readHeader() throws FileException {
		try {
			byte[] headerBytes = new byte[FileHeader.SIZE_IN_BYTES];
			this.file.seek(0);
			if (file.read(headerBytes) < FileHeader.SIZE_IN_BYTES) {
				throw new FileException("Error al leer el header: header incompleto");
			}
			this.header = FileHeader.fromByteArray(headerBytes);
		} catch (IOException e) {
			throw new FileException("Error al leer el header", e);
		}
	}
	
	private void writeHeader() throws FileException {
		try {
			file.seek(0);
			file.write(header.toByteArray());
		} catch (IOException e) {
			throw new FileException("Error al escribir el header", e);
		}
	}
	
	private int calculateDiskOffset(int blockPos) {
		return FileHeader.SIZE_IN_BYTES + blockPos * header.getBlockSize();
	}
	
	public static final float DEFAULT_MAX_BLOCK_LOAD_FACTOR_FOR_INSERT = 0.8f;
	
	public static final int DEFAULT_BLOCK_SIZE = 4096;
}